package org.zn.note.java8;

import org.zn.note.java8.Bean.Dish;

import java.util.*;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import static java.util.Comparator.comparingInt;
import static java.util.stream.Collectors.*;

/**
 * 用流收集数据
 * {@link Collectors}
 * <p>
 * {@link Stream#collect(Collector)} 收集数据
 * 在 {@link java.util.stream.Collectors} 里定义了很多Collector可以用
 * <p>
 * {@link Collectors#maxBy(Comparator)}
 * {@link Collectors#minBy(Comparator)}
 * {@link Collectors#counting()}
 * {@link Collectors#summarizingInt}
 * {@link Collectors#joining()}
 * {@link Collectors#reducing(BinaryOperator)}
 * <p>
 * 收集collect和归约reduce
 * 这里发现collect的事情，reduce也能做，那么有什么区别呢？
 * reduce是将多个数合并成一个新数，是一个不可变的归约
 * collect是通过改变容器，从而累计要输出的结果
 * 如果用错了，后面流的并行的时候，也有问题！
 * 所以：
 * 不可变归约，用reduce
 * 可变容器的归约，用collect
 * <p>
 * 分组 {@link Collectors#groupingBy(Function)}  {@link #testGroupBy}
 *
 * 分区 {@link Collectors#partitioningBy(Predicate)}
 */
public class Ch06_Collectors {

    private static final List<Dish> menu = Dish.genDishs();

    public static void main(String[] args) {
//        testGroupBy();
        testPartitioningBy();
    }

    private static void testCollect() {

        // 菜单有多少道菜
        long howManyDishes1 = menu.stream().collect(counting());
        long howManyDishes2 = menu.stream().count();

        // 看菜单中卡路里最高的菜
        Optional<Dish> mostCalorieDish =
                menu.stream().collect(maxBy(comparingInt(Dish::getCalories)));

        // 看所有菜品总热量
        int totalCalories =
                menu.stream().collect(summingInt(Dish::getCalories));

        // 收集热量的统计信息
        IntSummaryStatistics menuStatistics =
                menu.stream().collect(summarizingInt(Dish::getCalories));

        // 所有菜名
        // joining = a.toString, b.toString, ...
        String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));
    }

    /**
     * 分组
     */
    private static void testGroupBy() {
        // 分组：按菜品类型分组
        Map<Dish.Type, List<Dish>> dishGroupByType =
                menu.stream().collect(groupingBy(Dish::getType)); // Map<Dish.Type, List<Dish>>
        System.out.println(dishGroupByType);
        // 分组：按卡路里分组
        Map<String, List<Dish>> dishGroupByType2 = menu.stream().collect(groupingBy(dish -> {
            if (dish.getCalories() < 400)
                return "LOW";
            else if (dish.getCalories() < 600)
                return "MID";
            else
                return "HIG";
        }));
        System.out.println(dishGroupByType2);
        // 分组，先按菜品类型分组，在按卡路里分组
        menu.stream().collect(
                groupingBy(Dish::getType, groupingBy(
                        dish -> {
                            if (dish.getCalories() < 400)
                                return "LOW";
                            else if (dish.getCalories() < 600)
                                return "MID";
                            else
                                return "HIG";
                        })));
        // 分组，按菜品类型分组，查看每组菜品个数
        menu.stream().collect(groupingBy(Dish::getType, counting()));
        // 分组，按菜品类型分组，查看每组中卡路里最高的
        menu.stream().collect(groupingBy(Dish::getType, maxBy(comparingInt(Dish::getCalories))));
        // 分组，用mapping实现
        menu.stream().collect(groupingBy(Dish::getType, mapping(dish -> {
            if (dish.getCalories() < 400)
                return "LOW";
            else if (dish.getCalories() < 600)
                return "MID";
            else
                return "HIG";
        }, toSet())));
    }

    /**
     * 分区
     */
    private static void testPartitioningBy() {
        // 在素食/非素食两个分区中，按菜品类型进行分组
        Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType =
                menu.stream().collect(
                        partitioningBy(Dish::isVegetarian,  // 先按素食/非素食分区
                                groupingBy(Dish::getType))); // 然后再分组
        System.out.println(vegetarianDishesByType);
        // 素食和非素食中热量最高的菜
        Map<Boolean, Dish> mostCaloricPartitionedByVegetarian =
                menu.stream().collect(
                        partitioningBy(Dish::isVegetarian,
                                collectingAndThen(
                                        maxBy(comparingInt(Dish::getCalories)),
                                        Optional::get)));
        System.out.println(mostCaloricPartitionedByVegetarian);
        // 将100以内的自然数按质数/合数分区
        Map<Boolean, List<Integer>> m =
                IntStream.rangeClosed(2, 100) // 1既不是质数也不是合数，不算1
                        .boxed() // int流转成 Stream<Integer>
                        .collect( // 收集器
                                partitioningBy(i -> isPrime(i))); // 按质数/合数分区
        System.out.println(m);
    }

    // 是质数(素数)：一个大于1的自然数，除了1和它自身外，不能被其他自然数整除的数叫做质数
    // 1既不是质数也不是合数，其他的数叫合数
    public static boolean isPrime(int candidate) {
        // candidate是待测试的数
        // 从2开始找因数，只需要找到小于等于candidate平方根的就可以
        int candidateRoot = (int) Math.sqrt((double) candidate); // 最接近平方根的整数
        return IntStream.rangeClosed(2, candidateRoot) // [2, N] 的整数流
                .noneMatch(i -> candidate % i == 0); // 不包含candidate能整除的数
    }
}
